Source code for hysop.topology.topology

# Copyright (c) HySoP 2011-2024
#
# This file is part of HySoP software.
# See "https://particle_methods.gricad-pages.univ-grenoble-alpes.fr/hysop-doc/"
# for further info.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.


"""
Definitions for hysop topologies
* :class:`~hysop.topology.topology.TopologyState`
* :class:`~hysop.topology.topology.TopologyView`
* :class:`~hysop.topology.topology.Topology`
"""

from itertools import count
from abc import ABCMeta, abstractmethod
from hysop.constants import np, math, Backend
from hysop.constants import HYSOP_ORDER, BoundaryCondition
from hysop.constants import HYSOP_MPI_REAL
from hysop.domain.domain import Domain
from hysop.core.mpi import MPI
from hysop.core.arrays.array_backend import ArrayBackend
from hysop.tools.htypes import check_instance, to_tuple, first_not_None
from hysop.tools.parameters import MPIParams
from hysop.tools.misc import Utils
from hysop.tools.decorators import debug
from hysop.tools.numpywrappers import npw
from hysop.tools.string_utils import prepend
from hysop.tools.handle import RegisteredObject, TaggedObject, TaggedObjectView
from hysop.tools.warning import HysopWarning


[docs] class TopologyWarning(HysopWarning): """ Custom warning class for topology objects. """ pass
[docs] class TopologyState(TaggedObject, metaclass=ABCMeta): """ Abstract base to define TopologyStates. A TopologyState is a topology dependent state, and acts as a virtual state that determines how we should perceive raw mesh data. A TopologyState may for example include a transposition state like for CartesianTopology topologies. """ __slots__ = ("_is_read_only",) @debug def __new__(cls, is_read_only, **kwds): return super().__new__(cls, tag_prefix="ts", **kwds) @debug def __init__(self, is_read_only, **kwds): """Initialize a topology state.""" check_instance(is_read_only, bool) super().__init__(tag_prefix="ts", **kwds) self._is_read_only = is_read_only def _get_is_read_only(self): """Return true if this topology is read only.""" return self._is_read_only is_read_only = property(_get_is_read_only)
[docs] @abstractmethod def match(self, other, invert=False): """Check if this topology state does match the other one.""" res = self._is_read_only == other._is_read_only return (not res) if invert else res
@abstractmethod def __hash__(self): return hash(self._is_read_only)
[docs] @abstractmethod def copy(self, is_read_only=None, **kwds): """Return a copy of self, some properties may be alterted in kwds.""" pass
[docs] @abstractmethod def short_description(self): """Short description of this topology state.""" pass
[docs] @abstractmethod def long_description(self): """Long description of this topology state.""" pass
def __eq__(self, other): return self.match(other) def __ne__(self, other): return self.match(other, invert=True)
[docs] def __str__(self): """Same as self.long_description()""" return self.long_description()
[docs] class TopologyView(TaggedObjectView, metaclass=ABCMeta): """ Abstract base to define views on a Topology dependening on a TopologyState. A TopologyView is a view on a Topology altered by a TopologyState. It is a lightweight object that keeps a reference on a Topology and a TopologyState. A CartesianTopologyState may for example include a transposition state for CartesianTopology topologies, resulting in the automatic permutation of attributes when fetching the view attributes (global_resolution and ghosts will be transposed). """ __slots__ = ("_mesh_view", "_domain_view", "_topology", "_topology_state") @debug def __init__(self, topology_state, topology=None, **kwds): super().__init__(obj_view=topology, **kwds)
[docs] @debug def __new__(cls, topology_state, topology=None, **kwds): r""" Create and initialize a TopologyView on given topology. Parameters ---------- topology_state: :class:`~hysop.topology.topology.TopologyState` State that charaterizes the given view. topology: :class:`~hysop.topology.topology.Topology` Original topology on which the view is. kwds: dict Base class keyword arguments. Attributes ---------- topology: :class:`~hysop.topology.topology.Topology` Original topology on which the view is. topology_state: :class:`~hysop.topology.topology.TopologyState` State that charaterizes the given view. domain : :class:`~hysop.domain.domain.Domain` The geometry on which the topology is defined. backend: :class:`~hysop.core.arrays.array_backend.ArrayBackend` ArrayBackend of this topology. mpi_params: :class:`~hysop.tools.parameters.MPIParams` The parent MPI parameters of this topology. /!\ Topologies may define a sub communicator (CartesianTopology topologies will define a MPI.Cartcomm for example). parent: :class:`~hysop.core.mpi.IntraComm` Return the parent communicator used to build this topology. task_id: int Returns id of the task that owns this topology. Notes ----- All attributes are read-only properties. """ check_instance(topology, Topology, allow_none=True) check_instance(topology_state, TopologyState) obj = super().__new__(cls, obj_view=topology, **kwds) topology = topology or obj check_instance(topology, Topology) obj._topology = topology obj._topology_state = topology_state obj._domain_view = topology._domain.view(topology_state) if hasattr(topology, "_mesh"): obj.__set_mesh(topology._mesh) else: # mesh will be set later with __set_mesh. obj._mesh_view = None return obj
def __set_mesh(self, mesh): """Set mesh and build mesh view.""" if (not hasattr(self, "_mesh_view")) or (self._mesh_view is None): self._mesh_view = mesh.view(self._topology_state) def _get_topology(self): """Original topology on which the view is.""" return self._topology def _get_mesh(self): """Return a mesh view on local mesh.""" return self._mesh_view def _get_topology_state(self): """Original topology on which the view is.""" return self._topology_state def _get_backend(self): """ArrayBackend of this topology.""" return self._topology._backend def _get_mpi_params(self): r"""The parent MPI parameters of this topology. /!\ Topologies may define a sub communicator CartesianTopology topologies will define a MPI.Cartcomm for example. """ return self._topology._mpi_params def _get_domain(self): """The geometry on which the topology is defined.""" return self._domain_view def _get_domain_dim(self): """The geometry dimension on which the topology is defined.""" return self._topology._domain.dim @abstractmethod def _get_comm(self): """Return the communicator of this topology.""" pass def _get_parent(self): """Return the communicator used to build this topology.""" return self._topology._mpi_params.comm def _get_task_id(self): """Returns id of the task that owns this topology.""" return self._topology._mpi_params.task_id
[docs] @abstractmethod def default_state(self): """Return the default topology state of this topology.""" pass
[docs] @abstractmethod def short_description(self, topo): """Short description of this topology.""" pass
[docs] @abstractmethod def long_description(self, topo): """Long description of this topology.""" pass
[docs] def match(self, other, invert=False): """Check if two TopologyViews are equivalent.""" if not isinstance(other, TopologyView): return NotImplemented eq = self._topology is other._topology eq &= self._topology_state == other._topology_state if invert: return not eq else: return eq
def __eq__(self, other): return self.match(other) def __ne__(self, other): return self.match(other, invert=True) def __hash__(self): return id(self._topology) ^ hash(self._topology_state)
[docs] def __str__(self): """Same as self.long_description().""" return self.long_description(self.topology)
topology = property(_get_topology) topology_state = property(_get_topology_state) backend = property(_get_backend) mpi_params = property(_get_mpi_params) domain = property(_get_domain) mesh = property(_get_mesh) domain_dim = property(_get_domain_dim) parent = property(_get_parent) task_id = property(_get_task_id)
[docs] class Topology(RegisteredObject, metaclass=ABCMeta): """ Abstract base class for hysop Topologies. In hysop, a topology is defined as the association of a mpi process distribution (mpi topology) and of a set of local meshes (one per process). At the time, only CartesianTopology topologies with cartesian meshes are available. For details about topologies see HySoP User Manual. You can also find examples of topologies instanciation in test_topology.py. """
[docs] @debug def __new__( cls, domain, mpi_params=None, backend=Backend.HOST, cl_env=None, allocator=None, queue=None, **kwds, ): """ Creates or get an existing topology. Parameters ---------- domain : :class:`~hysop.domain.domain.Domain` the geometry on which the topology is defined. mpi_params : :class:`~hysop.tools.parameters.MPIParams`, optional MPI parameters (comm, task ...). If not specified, comm = domain.task_comm, task = domain.curent_task() backend: :class:`~hysop.constants.Backend` or `~hysop.core.arrays.ArrayBackend`, optional Backend or backend kind for this topology. By default a topology will use Backend.HOST. allocator: :class:`~hysop.core.memory.allocator.Allocator`, optional Allocated used on HOST backends instead of the default host memory pool allocator. Only used if the backend is Backend.HOST. cl_env: :class:`~hysop.backend.device.opencl.opencl_env.OpenClEnvironment`, optional Topology will use this specified OpenClEnvironment when backend is Backend.OPENCL. By default a new environment is created with get_or_create_opencl_env(mpi_params.comm) queue: :class:`pyopencl.Queue`, optional Default queue used for OpenCL Arrays. By default this queue will be cl_env.default_queue. Only used if the backend is Backend.OPENCL. kwds: dict Base class keyword arguments. Notes: ------ Topologies can be uniquely identified by their id as they are hysop RegisteredObjects. See :class:`~hysop.tools.handle.RegisteredObject` for more information. """ # Create MPI parameters if necessary mpi_params = cls._create_mpi_params(mpi_params, domain, cl_env) # Create backend if necessary backend = cls._create_backend(backend, mpi_params, allocator, cl_env, queue) # double check types, to be sure RegisteredObject will work as expected check_instance(mpi_params, MPIParams) check_instance(backend, ArrayBackend) check_instance(domain, Domain) obj = super().__new__( cls, backend=backend, mpi_params=mpi_params, domain=domain, tag_prefix="t", **kwds, ) if not obj.obj_initialized: obj._backend = backend obj._mpi_params = mpi_params obj._domain = domain obj._domain.register_topology(obj) return obj
@classmethod def _create_mpi_params(cls, mpi_params, domain, cl_env): if mpi_params is None: if cl_env is None: mpi_params = MPIParams( comm=domain.task_comm, task_id=domain.current_task() ) else: mpi_params = cl_env.mpi_params task_id = mpi_params.task_id msg = "MPI task_id contained in mpi_params parameter is None." assert task_id is not None, msg msg = "Current process is not on task {} but on task {}." msg = msg.format(task_id, domain.current_task()) assert domain.is_on_task(task_id), msg return mpi_params @classmethod def _create_backend(cls, backend, mpi_params, allocator, cl_env, queue): if isinstance(backend, Backend): if backend == Backend.HOST: assert cl_env is None assert queue is None from hysop.core.arrays.all import HostArrayBackend backend = HostArrayBackend.get_or_create(allocator) elif backend == Backend.OPENCL: from hysop.backend.device.opencl.opencl_tools import ( get_or_create_opencl_env, ) from hysop.core.arrays.all import OpenClArrayBackend if cl_env is None: cl_env = get_or_create_opencl_env(mpi_params) assert cl_env.mpi_params == mpi_params backend = OpenClArrayBackend.get_or_create( cl_env=cl_env, queue=queue, allocator=allocator ) assert backend.cl_env.mpi_params == mpi_params else: msg = f"Unsupported backend {backend}." raise ValueError(msg) elif isinstance(backend, ArrayBackend): assert cl_env is None assert allocator is None assert queue is None if (backend.kind == Backend.OPENCL) and ( backend.cl_env.mpi_params != mpi_params ): msg = "mpi_params mismatch between backend and topology." raise ValueError(msg) else: msg = f"Unknown backend type {type(backend)}." return backend
[docs] @abstractmethod def view(self, topology_state): """Return a TopologyView of this topology with a specific state.""" pass
[docs] @abstractmethod def discretize(self, field): """Discretize a continous field on this topology and return a DiscreteField.""" pass